Skip to content

feat(billing): charge and report Stripe service fees - #5383

Open
jeanduplessis wants to merge 27 commits into
mainfrom
transaction-service-fee-all-in-one
Open

feat(billing): charge and report Stripe service fees#5383
jeanduplessis wants to merge 27 commits into
mainfrom
transaction-service-fee-all-in-one

Conversation

@jeanduplessis

Copy link
Copy Markdown
Contributor

Summary

Adds the complete Stripe service-fee implementation for eligible credit top-ups and Kilo Pass purchases, with durable fee assessments, trusted product-principal settlement, exemptions, lifecycle adjustments, fee-only reporting, and rollout audits.

Why this change is needed

A 5% Stripe line item alone is not enough to operate the fee safely. The platform must distinguish product principal from the added fee so credits, entitlements, affiliate value, and product analytics do not use gross payment amounts. It must also retain fee decisions when no line is charged, survive Stripe webhook ordering and retries, handle refunds and disputes, support reasoned organization exemptions, and report collected or missed fees without turning the sidecar into a second payment ledger.

This PR replaces the six-PR stack in #5236, #5262, #5263, #5264, #5265, and #5266 so the complete behavior can be reviewed and merged as one unit.

How this is addressed

  • Add an additive two-table schema: one service-fee assessment per commercial event and an append-only organization exemption history.
  • Calculate and attach a separate 5% fee line to eligible personal and organization top-ups and Kilo Pass purchases, including recurring and automatic invoice flows.
  • Keep product settlement independent from gross Stripe amounts so service fees never increase credits, entitlements, affiliate commission, or product analytics.
  • Fail open when a positive fee cannot be prepared or attached while retaining a durable missed-fee assessment for audit and reporting.
  • Enrich one assessment across Checkout, Invoice, PaymentIntent, Charge, refund, and dispute events with idempotent, order-tolerant state transitions.
  • Allocate fee refunds proportionally and track reversible fee disputes without persisting disputed product principal.
  • Add exact-organization exemption controls with immutable reasons and historical decision references.
  • Preserve the existing credit_transactions revenue series and report assessment-backed fee metrics separately by UTC settlement date, including collected, missed, exempted, and disputed amounts.
  • Add read-only Kilo Pass classification and restricted-coupon audits for rollout safety.

Human Verification

  • Compared the final implementation requirement by requirement with .plans/service-fees/PRAGMATIC-SIDECAR.md and confirmed the fee sidecar does not become a product or payment ledger.
  • Verified the legacy paid, free, multiplied, and unmultiplied credit-revenue SQL remains unchanged from its pre-service-fee baseline; fee reporting is additive.
  • Applied all migrations successfully to an empty PostgreSQL database.
  • Passed 161 focused revenue, service-fee, persistence, dispute, and admin-dashboard tests.
  • Passed 1,118 broader Stripe, Kilo Pass, credits, organization, affiliate, and email tests, with 2 skipped.
  • Exercised /admin/revenue with fake local data: assessment-backed credits remained in Paid Revenue, Kilo Pass produced a fee-only UTC day, refund/dispute overlap clamped collected fees to zero, empty ranges rendered correctly, and CSV output used the narrowed fee-only shape.

Reviewer Notes

Human Reviewer Flags

  • The assessment table is intentionally a narrow fee-decision sidecar. Stripe remains authoritative for charges, invoices, refunds, and disputes, while credit_transactions remains authoritative for the existing product-revenue series.
  • Positive fee failures fail open: the underlying purchase proceeds and the missed expected fee remains auditable. No later corrective charge is created automatically.
  • Hosted Checkout discounts may reduce the settled fee below the prepared fee. Expected and charged fee amounts therefore remain separate.
  • Organization exemptions are exact-organization, non-inherited, append-only, and internal; they are not exposed through customer-facing audit logs.
  • Kilo Pass contributes service-fee metrics only. This change does not add Kilo Pass product revenue or claim authoritative gross revenue in the admin dashboard.
  • A human-driven Stripe test-mode dispute lifecycle remains a release validation step because Stripe CLI cannot attach a dispute to an existing charge.

Code Reviewer Agent

Code Reviewer Notes
  • Review the trusted-principal boundary before route wiring: product value comes from classified product lines and metadata, never from gross paid minus an assumed fee.
  • Checkout, Kilo-owned auto-top-up invoices, and Stripe-advanced Kilo Pass invoices have distinct attachment ownership to prevent duplicate fee lines.
  • Unique Stripe-object indexes and transactional assessment updates make webhook retries idempotent while permitting out-of-order enrichment.
  • Refund allocation is cumulative to avoid partial-refund rounding drift. Collected fee reporting clamps each assessment with GREATEST(charged - refunded - disputed, 0) before aggregation.
  • Fee reporting is gated by settled_at and grouped in UTC. Assessment rows are never anti-joined out of the existing credit-revenue query.
  • Migration 0221 is additive and unshipped: it creates only the assessment and exemption tables, with generated snapshot metadata.

jeanduplessis and others added 25 commits August 19, 2026 15:55
Introduce the assessment and exemption tables, plus the operator
runbook and ADR that define one assessment per commercial event.
No runtime billing behavior changes yet.
On-call runbooks live in kilo-org/on-call. Leave the ADR here as the
engineering record.
Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com>
Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com>
Land the fee math, durable assessments, Checkout/invoice attach helpers,
and refund/dispute observers without wiring them into payment paths.
Pin the Stripe API version so line-shape assumptions stay stable.
Stop treating Stripe gross paid amounts as product value. Settlement
now reads a trusted principal, classifiers ignore fee lines, and
Impact Kilo Pass sales use the product amount. No fee is charged yet.
Attach the 5% fee to personal and organization Checkout top-ups and
auto top-up invoices. Refund and dispute webhooks now observe fee
assessments. Kilo Pass charging is still unwired.
Attach fees to personal and organization Kilo Pass Checkout and
Stripe-advanced invoices, including org seat-capacity updates.
Add the org exemption admin UI, fee-vs-product revenue reporting, and
read-only Kilo Pass classification and restricted-coupon audits.
@IamCoder18

Copy link
Copy Markdown
Contributor

Are no Stripe fees being charged until November? The email said "September 1". If so, this would be great!

@jeanduplessis

Copy link
Copy Markdown
Contributor Author

Are no Stripe fees being charged until November? The email said "September 1". If so, this would be great!

@IamCoder18 there is a chance that it will move to 1 Nov. We'll know for sure next week.

@jeanduplessis
jeanduplessis requested a review from jrf0110 August 20, 2026 08:00
@kilo-code-bot

kilo-code-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 5
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/lib/service-fees/stripe-lines.ts 88 Checkout fee identity lives on product metadata; invoice detection only reads line.metadata, so invoice.created can attach a second 5% fee

WARNING

File Line Issue
apps/web/src/lib/service-fees/invoice-created.ts 128 Webhook attach is skipped only for non-invoice: keys, so personal Checkout's first invoice is keyed invoice:<id> and still attaches
apps/web/src/lib/kilo-pass/stripe-handlers-invoice-paid.ts 436 Settlement failure reports $0 product whenever a service-fee line is present, under-reporting affiliate/analytics
apps/web/src/lib/service-fees/disputes.ts 100 Closed-won before funds_withdrawn is a no-op, so a late withdraw permanently leaves the fee disputed
apps/web/src/app/admin/components/RevenueStats.tsx 32 Credit KPI averages and Latest day include fee-only UTC rows from the FULL OUTER JOIN
apps/web/src/lib/service-fees/refunds.ts 323 A complete credit-note allocation of $0 product and $0 fee is treated as unresolved
Files Reviewed (24 files)
  • apps/web/src/lib/service-fees/stripe-lines.ts - 1 issue
  • apps/web/src/lib/service-fees/invoice-created.ts - 1 issue
  • apps/web/src/lib/kilo-pass/stripe-handlers-invoice-paid.ts - 1 issue
  • apps/web/src/lib/service-fees/disputes.ts - 1 issue
  • apps/web/src/app/admin/components/RevenueStats.tsx - 1 issue
  • apps/web/src/lib/service-fees/refunds.ts - 1 issue
  • apps/web/src/lib/service-fees/calculation.ts
  • apps/web/src/lib/service-fees/assessments.ts
  • apps/web/src/lib/service-fees/settlement.ts
  • apps/web/src/lib/service-fees/drizzle-store.ts
  • apps/web/src/lib/service-fees/checkout.ts
  • apps/web/src/lib/kilo-pass-org/stripe-adapter.ts
  • apps/web/src/routers/kilo-pass-router.ts
  • apps/web/src/lib/stripe/index.ts
  • apps/web/src/lib/revenueKpi.ts
  • apps/web/src/lib/credits.ts
  • apps/web/src/lib/kilo-pass/affiliate-sale.ts
  • apps/web/src/routers/organizations/organization-admin-router.ts
  • packages/db/src/schema.ts
  • packages/db/src/migrations/0221_goofy_true_believers.sql
  • apps/web/src/lib/service-fees/organization-exemptions.ts
  • apps/web/src/lib/service-fees/alerts.ts
  • apps/web/src/lib/service-fees/tax.ts
  • docs/adr/0004-stripe-service-fee-assessment.md

Reviewed by grok-4.6 · Input: 802.9K · Output: 70.5K · Cached: 2.4M

Review guidance: REVIEW.md from base branch main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants